﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
using Majoopic.Service.Interface;
using Majoopic.Service.Model;
using Majoopic.Utility.Image;

namespace Majoopic.Engine
{
    /// <summary>
    /// Engine to extract picture from physical device (HDD, SDD, USB
    /// </summary>
    public class FileEngine
    {
        /// <summary>
        /// Struct use by the imagesToScanJob
        /// </summary>
        internal struct ImageDirectory
        {
            internal string directory;
            internal string[] fileNames;
        }

        #region Members
        private BlockingCollection<ImageDirectory> _imagesToScan;

        private BlockingCollection<ImageInformation> _imagesToSave;

        private object _messageLocker = new object();

        private CancellationTokenSource _cancellationToken;
        
        private ImageDecoder _imageDecoder;
        
        private Task currentTask;
        
        /// <summary>
        /// Cache with all the messages generated
        /// </summary>
        private List<Message> _ErrorMessages;

        /// <summary>
        /// Service to save a picture in the database
        /// To avoid threading problem, we create one instance for each thread
        /// </summary>
        private ThreadLocal<IPictureService> _PictureService;
        
        
        // No need 
        //private object _dispatcherLocker = new object();
        //private Dispatcher _callerDispatcher;

        #endregion

        #region Properties
        /// <summary>
        /// Root path of the directory
        /// </summary>
        public string RootPath
        {
            get;
            private set;
        }

        /// <summary>
        /// Scan subfolder or not
        /// </summary>
        public bool IncludeSubFolders
        {
            get;
            private set;
        }

        /// <summary>
        /// Extension of file to scan
        /// </summary>
        public string SearchPattern
        {
            get;
            private set;
        }

        /// <summary>
        /// The context of the current application
        /// </summary>
        public ServiceContext CurrentContext
        {
            get;
            private set;
        }

        private object _WaitForDirectoryAccessLocker = new object();
        private bool _WaitForDirectoryAccess = false;
        /// <summary>
        /// Use to stop a IO access.
        /// If the parent need to access the hard disk is better to stop the IO access in the job thread
        /// </summary>
        public bool WaitForDirectoryAccess 
        {
            get
            {
                lock (this._WaitForDirectoryAccessLocker)
                {
                    return this._WaitForDirectoryAccess;
                }
            }
            set
            {
                lock (this._WaitForDirectoryAccessLocker)
                {
                    this._WaitForDirectoryAccess = value;
                }
            }
        }
  
        private event EventHandler<MessageEventArgs> _MessageAdded;
        /// <summary>
        /// Event publish when a new message is generated
        /// Events are publish on the caller thread        
        /// </summary>
        public event EventHandler<MessageEventArgs> MessageAdded
        {
            add
            {
                this._MessageAdded += value;
            }
            remove
            {
                this._MessageAdded -= value;
            }
        }

        private event EventHandler<ImageInformationEventArgs> _ImageScanned;
        /// <summary>
        /// Event publish when an image is scanned
        /// </summary>
        public event EventHandler<ImageInformationEventArgs> ImageScanned
        {
            add
            {
                this._ImageScanned += value;
            }
            remove
            {
                this._ImageScanned -= value;
            }
        }

        private event EventHandler _TaskFinished;
        /// <summary>
        /// Event publish when a task is finish     
        /// </summary>
        public event EventHandler TaskFinished
        {
            add
            {
                this._TaskFinished += value;
            }
            remove
            {
                this._TaskFinished -= value;
            }
        }

        private event EventHandler _ScanDirectoryFinished;
        /// <summary>
        /// Event publish when the task scandirectory is finished     
        /// </summary>
        public event EventHandler ScanDirectoryFinished
        {
            add
            {
                this._ScanDirectoryFinished += value;
            }
            remove
            {
                this._ScanDirectoryFinished -= value;
            }
        }
       

        /// <summary>
        /// Information about the current progression of a job
        /// </summary>
        public ProgressInformation ProgressInformation
        {
            get;
            private set;
        }
        
        #endregion

        #region Constructor
        /// <summary>
        /// Constructor
        /// </summary>
        public FileEngine(ServiceContext currentContext, string rootPath, bool includeSubFolders)
        {
            if (currentContext == null || string.IsNullOrWhiteSpace(rootPath))
            {
                throw new ArgumentNullException();
            }
            this.CurrentContext = currentContext;
            this.RootPath = rootPath;
            this.IncludeSubFolders = includeSubFolders;
            this.SearchPattern = "*.jpg";
            this._cancellationToken = new CancellationTokenSource();
            this._imageDecoder = new ImageDecoder();
            this._imagesToSave = new BlockingCollection<ImageInformation>();
            this._imagesToScan = new BlockingCollection<ImageDirectory>();
            this._ErrorMessages = new List<Message>();
            this._PictureService = new ThreadLocal<IPictureService>(() => currentContext.CurrentFactory.GetPictureService());
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Start the job to add all picture from the directory to the library
        /// </summary>
        public void StartAsyncJob(ProgressInformation progressInformation)
        {            
            if(this.currentTask!=null && this.currentTask.Status == TaskStatus.Running)
            {
                throw new InvalidOperationException("A task is already running");
            }
            
            //lock (this._dispatcherLocker)
            //{
            //    this._callerDispatcher = Dispatcher.CurrentDispatcher;
            //}
            this.ProgressInformation = progressInformation;
            if (Directory.Exists(this.RootPath) == false)
            {
                throw new DirectoryNotFoundException(string.Format("Directory not found : {0}", this.RootPath));
            }
            this.currentTask = new Task(() => this.StartJob(), this._cancellationToken.Token, TaskCreationOptions.LongRunning);
            this.currentTask.Start();
            this.currentTask.ContinueWith(t => { if (this._TaskFinished != null)this._TaskFinished(this, EventArgs.Empty); }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
        }

        /// <summary>
        /// Cancel the current job
        /// </summary>
        public void Cancel()
        {
            this._cancellationToken.Cancel();
        }

        /// <summary>
        /// Get the actual list of all the messages
        /// </summary>
        /// <returns></returns>
        public List<Message> GetErrorMessages()
        {
            // We create a copy of the actual list
            // Instance of Message are not thread safe, but the messages are not changed after first creation
            List<Message> messages = new List<Message>();
            lock (this._messageLocker)
            {
                messages.AddRange(this._ErrorMessages);
            }
            return messages;
        }
        #endregion

        #region Private Methods
        private void StartJob()
        {
            try
            {
                if (this.ProgressInformation != null)
                    this.ProgressInformation.IsScanDirectory = true;
                // Start scanning directory and queue images in jobs
                this.ScanDirectory(this.RootPath);
               
                if (this._ScanDirectoryFinished != null)
                {
                    this._ScanDirectoryFinished(this, EventArgs.Empty);
                }
            }
            catch (OperationCanceledException e)
            {
                this.AddError("ScanDirectory Canceled", e);
                return;
            }
            finally
            {
                if (this.ProgressInformation != null)
                    this.ProgressInformation.IsScanDirectory = false;
                this._imagesToScan.CompleteAdding();
            }


            Task[] tasks = new Task[4];

            // Starting 3 tasks to save images
            for (int i = 1; i < 4; i++)
                tasks[i] = Task.Factory.StartNew(SaveImagesJob);

            // Starting the task to scan each folder
            tasks[0] = Task.Factory.StartNew(ScanImagesJob, this._cancellationToken.Token);
                   
            //TODO : manage exception
            
            // We wait the end of children task
            Task.WaitAll(tasks);
        }

        private void ScanDirectory(string directory)
        {
            try
            {
                this.AddInformationMessage(string.Format("TaskID {1} : Scan the directory {0}", directory, Task.CurrentId));
                if(this.ProgressInformation != null) 
                    this.ProgressInformation.IncrementTotalDirectoryScan();
                string[] imagesName = Directory.GetFiles(directory, this.SearchPattern);
                if (imagesName.Length > 0)
                {
                    this._cancellationToken.Token.ThrowIfCancellationRequested();
                    // Queue the images in the imagesScanJob
                    ImageDirectory imageDir = new ImageDirectory();
                    imageDir.fileNames = imagesName;
                    imageDir.directory = directory;
                    this.AddInformationMessage(string.Format("TaskID {0} : Add {2} images from directory {1} to imagesToScan queue", Task.CurrentId, directory, imagesName.Length));
                    if (this.ProgressInformation != null)
                        this.ProgressInformation.IncrementTotalImagesFound(imagesName.Length);
                    // Add a job for this directory with the list of all images to scan
                    this._imagesToScan.Add(imageDir);
                }
                // Look for images in subdirectory
                foreach (string subDirectory in Directory.GetDirectories(directory))
                {                    
                    this.ScanDirectory(Path.Combine(directory, subDirectory));
                }
            }
            catch (UnauthorizedAccessException e)
            {
                // We continue in case of unauthorized access on a folder.
                // But we trace the folder
                this.AddError(string.Format("Unauthorized access to the directory : {0}", directory), e);
            }            
        }

        private void ScanImagesJob()
        {
            try
            {
                this.AddInformationMessage(string.Format("TaskID {0} : Task ScanImagesJob started", Task.CurrentId));
                // Waiting for some job to do in the queue
                foreach (ImageDirectory directory in this._imagesToScan.GetConsumingEnumerable(this._cancellationToken.Token))
                {
                    this.ScanImages(directory);
                }
            }
            catch (OperationCanceledException e)
            {
                this.AddError("ScanImagesJob Canceled", e);
            }
            finally
            {
                this._imagesToSave.CompleteAdding();
            }
        }

        private void ScanImages(ImageDirectory directory)
        {
            this.AddInformationMessage(string.Format("TaskID {2} : Scan {0} images in directory {1}", directory.fileNames.Length, directory.directory, Task.CurrentId));
            foreach (string fileName in directory.fileNames)
            {
                this._cancellationToken.Token.ThrowIfCancellationRequested();
                try
                {
                    if (this.ProgressInformation != null)
                        this.ProgressInformation.IncrementNumberImagesScaned();
                    this.ScanImage(directory.directory, fileName);
                }
                catch (Exception ex)
                {
                    if (this.ProgressInformation != null)
                        this.ProgressInformation.IncrementNumberImagesScanFailed();
                    this.AddError(string.Format("Error when scanning the image {0} in the directory {1}", directory.directory, fileName), ex);
                }
            }
        }

        private void ScanImage(string directory, string fileName)
        {

            this.AddInformationMessage(string.Format("TaskID {2} : Scan the image {0} from directory {1}", fileName, directory, Task.CurrentId));
            
            // se the image decoder to read the header information
            ImageHeaderInformation headerInfo = this._imageDecoder.GetImageHeaderInformation(Path.Combine(directory, fileName));
            ImageInformation imageInfo = new ImageInformation(directory,fileName,headerInfo);
            this.AddInformationMessage(string.Format("TaskID {2} : Add the image {0} from the directory {1} to the imagesToScan queue", fileName, directory, Task.CurrentId));
            this.RaiseImageScanned(imageInfo);
            // Add the imageinformation in the queue to be save
            this._imagesToSave.Add(imageInfo);
            if (this.ProgressInformation != null)
                this.ProgressInformation.IncrementTotalImagesToSave();
        }

        private void SaveImagesJob()
        {
            try
            {
                this.AddInformationMessage(string.Format("TaskID {0} : Task SaveImagesJob started", Task.CurrentId));
                // Waiting to have some job to do in the queue
                foreach (ImageInformation imageInfo in this._imagesToSave.GetConsumingEnumerable(this._cancellationToken.Token))
                {
                    try
                    {
                        if (this.ProgressInformation != null)
                            this.ProgressInformation.IncrementNumberImagesSaved();
                        this.SaveImage(imageInfo);
                    }
                    catch (Exception ex)
                    {
                        if (this.ProgressInformation != null)
                            this.ProgressInformation.IncrementNumberImagesSaveFailed();
                        this.AddError(string.Format("Error when saving the image {0} in the directory {1}", imageInfo.DirectoryPath, imageInfo.FileName), ex);
                    }
                }
            }
            catch (OperationCanceledException e)
            {
                this.AddError("SaveImagesJob Canceled", e);
            }
        }        

        private void SaveImage(ImageInformation imageInfo)
        {
            this.AddInformationMessage(string.Format("TaskID {2} : Save the image {0} from directory {1}", imageInfo.FileName, imageInfo.DirectoryPath, Task.CurrentId));
            IPictureService service = this._PictureService.Value;
            service.AddPicture(imageInfo);
        }

        private void AddError(string error, Exception exception)
        {
            this.AddMessages(MessageType.Error, error, exception);
        }

        private void AddInformationMessage(string informationMessage)
        {
            this.AddMessages(MessageType.Info,informationMessage,null);
        }

        private void AddMessages(MessageType messageType, string messageStr, Exception exception)
        {
            Message message = new Message(messageType, messageStr, exception);
            lock (this._messageLocker)
            {
                this._ErrorMessages.Add(message);
            }

            if (this._MessageAdded != null)
            {
                // Execute the event on the caller dispatcher
                //this._callerDispatcher.BeginInvoke(new Action(() => this._MessageAdded(this, new MessageEventArgs(message))));
                this._MessageAdded(this, new MessageEventArgs(message));
            }
        }
        #endregion

        #region RaiseEvents
        private void RaiseImageScanned(ImageInformation image)
        {
            if (this._ImageScanned != null)
            {
                this._ImageScanned(this, new ImageInformationEventArgs(image));
            }
        }
        #endregion
    }
}
